package com.atomjack.vcfp;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.AlertDialog;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import android.support.v7.media.MediaRouter;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
import com.android.vending.billing.IabBroadcastReceiver;
import com.android.vending.billing.IabHelper;
import com.android.vending.billing.IabResult;
import com.android.vending.billing.Inventory;
import com.android.vending.billing.Purchase;
import com.android.vending.billing.SkuDetails;
import com.atomjack.shared.Intent;
import com.atomjack.shared.Logger;
import com.atomjack.shared.NewLogger;
import com.atomjack.shared.PlayerState;
import com.atomjack.shared.Preferences;
import com.atomjack.shared.SendToDataLayerThread;
import com.atomjack.shared.UriDeserializer;
import com.atomjack.shared.UriSerializer;
import com.atomjack.shared.WearConstants;
import com.atomjack.vcfp.activities.MainActivity;
import com.atomjack.vcfp.interfaces.ActiveConnectionHandler;
import com.atomjack.vcfp.interfaces.BitmapHandler;
import com.atomjack.vcfp.model.Connection;
import com.atomjack.vcfp.model.MediaContainer;
import com.atomjack.vcfp.model.PlexClient;
import com.atomjack.vcfp.model.PlexDirectory;
import com.atomjack.vcfp.model.PlexMedia;
import com.atomjack.vcfp.model.PlexServer;
import com.atomjack.vcfp.model.PlexTrack;
import com.atomjack.vcfp.services.LocalMusicService;
import com.atomjack.vcfp.services.SubscriptionService;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.Wearable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import cz.fhucho.android.util.SimpleDiskCache;
public class VoiceControlForPlexApplication extends Application
{
private NewLogger logger;
public final static String MINIMUM_PHT_VERSION = "1.0.7";
private static boolean isApplicationVisible;
private static int nowPlayingNotificationId = 0;
private static VoiceControlForPlexApplication instance;
public Preferences prefs;
public static Gson gsonRead = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer())
.create();
public static Gson gsonWrite = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
// When scanning for servers, if a local server is found but is not accessible due to requiring login
// alert the user that one or more servers like this were found.
public List<String> unauthorizedLocalServersFound = new ArrayList<>();
private IabBroadcastReceiver promoReceiver;
public static HashMap<String, String[]> chromecastVideoQualityOptions = new LinkedHashMap<String, String[]>();
public static String chromecastVideoQualityDefault;
public static HashMap<String, String[]> localVideoQualityOptions = new LinkedHashMap<String, String[]>();
public static String localVideoQualityDefault;
private NotificationManager mNotifyMgr;
private Bitmap notificationBitmap = null;
private Bitmap notificationBitmapBig = null;
public static ConcurrentHashMap<String, PlexServer> servers = new ConcurrentHashMap<String, PlexServer>();
public static Map<String, PlexClient> clients = new HashMap<String, PlexClient>();
public static Map<String, PlexClient> castClients = new HashMap<String, PlexClient>();
public static Map<String, MediaRouter.RouteInfo> castRoutes;
private static Serializer serial = new Persister();
public SimpleDiskCache mSimpleDiskCache;
private int currentImageCacheVersion = 1;
private NetworkChangeListener networkChangeListener;
// In-app purchasing
private IabHelper mIabHelper;
private boolean iabHelperSetupDone = false;
String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlgV+Gdi4nBVn2rRqi+oVLhenzbWcEVyUf1ulhvAElEf6c8iuX3OB4JZRYVhCE690mFaYUdEb8OG8p8wT7IrQmlZ0DRfP2X9csBJKd3qB+l9y11Ggujivythvoiz+uvDPhz54O6wGmUB8+oZXN+jk9MT5Eia3BZxJDvgFcmDe/KQTTKZoIk1Qs/4PSYFP8jaS/lc71yDyRmvAM+l1lv7Ld8h69hVvKFUr9BT/20lHQGohCIc91CJvKIP5DaptbE98DAlrTxjZRRpbi+wrLGKVbJpUOBgPC78qo3zPITn6M6N0tHkv1tHkGOeyLUbxOC0wFdXj33mUldV/rp3tHnld1wIDAQAB";
private boolean inventoryQueried = false;
// Has the user purchased chromecast/wear support?
// This is the default value.
private boolean mHasChromecast = !BuildConfig.CHROMECAST_REQUIRES_PURCHASE;
private boolean mHasWear = !BuildConfig.WEAR_REQUIRES_PURCHASE;
private boolean mHasLocalMedia = !BuildConfig.LOCALMEDIA_REQUIRES_PURCHASE;
// Only the release build will use the actual Chromecast/Wear SKU
public static final String SKU_CHROMECAST = BuildConfig.SKU_CHROMECAST;
public static final String SKU_WEAR = BuildConfig.SKU_WEAR;
public static final String SKU_LOCALMEDIA = BuildConfig.SKU_LOCALMEDIA;
public static final String SKU_TEST_PURCHASED = "android.test.purchased";
private static String mChromecastPrice = "$3.00"; // Default price, just in case
private static String mWearPrice = "$2.00"; // Default price, just in case
private static String mLocalMediaPrice = "$2.00";
public static boolean hasDoneClientScan = false;
public Feedback feedback;
GoogleApiClient googleApiClient;
// This is needed so that we can let the main activity know that wear support is enabled, after querying the inventory from Google
private MainActivity mainActivity;
@Override
public void onCreate() {
super.onCreate();
instance = this;
logger = new NewLogger(this);
feedback = new Feedback(this);
googleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.build();
googleApiClient.connect();
prefs = new Preferences(getApplicationContext());
// chromecastVideoQualityOptions.put(getString(R.string.original), new String[]{"12000", "1920x1080", "1"}); // Disabled for now. Don't know how to get PMS to direct play to chromecast
chromecastVideoQualityDefault = "8mbps 720p";
chromecastVideoQualityOptions.put("20mbps 720p", new String[]{"20000", "1280x720"});
chromecastVideoQualityOptions.put("12mbps 720p", new String[]{"12000", "1280x720"});
chromecastVideoQualityOptions.put("10mbps 720p", new String[]{"10000", "1280x720"});
chromecastVideoQualityOptions.put("8mbps 720p", new String[]{"8000", "1280x720"});
chromecastVideoQualityOptions.put("4mbps 720p", new String[]{"4000", "1280x720"});
chromecastVideoQualityOptions.put("3mbps 720p", new String[]{"3000", "1280x720"});
chromecastVideoQualityOptions.put("2mbps 720p", new String[]{"2000", "1280x720"});
chromecastVideoQualityOptions.put("1.5mbps 720p", new String[]{"1500", "1280x720"});
localVideoQualityDefault = "8mbps 1080p";
localVideoQualityOptions.put("20mbps 1080p", new String[]{"20000", "1920x1080"});
localVideoQualityOptions.put("12mbps 1080p", new String[]{"12000", "1920x1080"});
localVideoQualityOptions.put("10mbps 1080p", new String[]{"10000", "1920x1080"});
localVideoQualityOptions.put("8mbps 1080p", new String[]{"8000", "1920x1080"});
localVideoQualityOptions.put("4mbps 720p", new String[]{"4000", "1280x720"});
localVideoQualityOptions.put("3mbps 720p", new String[]{"3000", "1280x720"});
localVideoQualityOptions.put("2mbps 720p", new String[]{"2000", "1280x720"});
localVideoQualityOptions.put("1.5mbps 720p", new String[]{"1500", "1280x720"});
localVideoQualityOptions.put("720kbps 480p", new String[]{"720", "852x480"});
localVideoQualityOptions.put("512kbps 480p", new String[]{"512", "852x480"});
localVideoQualityOptions.put("320kbps 480p", new String[]{"320", "852x480"});
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL) == null)
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL, "8mbps 720p");
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE) == null)
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE, "8mbps 720p");
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.LOCAL_VIDEO_QUALITY_LOCAL) == null)
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.LOCAL_VIDEO_QUALITY_LOCAL, "4mbps 720p");
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.LOCAL_VIDEO_QUALITY_REMOTE) == null)
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.LOCAL_VIDEO_QUALITY_REMOTE, "2mbps 720p");
// Check for donate version, and if found, allow chromecast & wear
PackageInfo pinfo;
try
{
pinfo = getPackageManager().getPackageInfo("com.atomjack.vcfpd", 0);
mHasChromecast = true;
mHasWear = true;
} catch(Exception e) {}
// If this build includes chromecast and wear support, no need to setup purchasing
if(hasAnyInAppPurchase())
setupInAppPurchasing();
mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// Load saved clients and servers
Type clientType = new TypeToken<HashMap<String, PlexClient>>(){}.getType();
VoiceControlForPlexApplication.clients = gsonRead.fromJson(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.SAVED_CLIENTS, "{}"), clientType);
Type serverType = new TypeToken<ConcurrentHashMap<String, PlexServer>>(){}.getType();
VoiceControlForPlexApplication.servers = gsonRead.fromJson(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.SAVED_SERVERS, "{}"), serverType);
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
mSimpleDiskCache = SimpleDiskCache.open(getCacheDir(), pInfo.versionCode, Long.parseLong(Integer.toString(10 * 1024 * 1024)));
checkImageCacheVersion();
Logger.d("Cache initialized");
} catch (Exception ex) {
ex.printStackTrace();
}
}
public boolean hasChromecast() {
return mHasChromecast;
}
public boolean hasWear() {
return mHasWear;
}
public boolean hasLocalmedia() {
return mHasLocalMedia;
}
public boolean hasAnyInAppPurchase() {
return !hasChromecast() || !hasWear() || !hasLocalmedia();
}
public static VoiceControlForPlexApplication getInstance() {
return instance;
}
public static Locale getVoiceLocale(String loc) {
if(loc == null)
return new Locale(Locale.getDefault().getISO3Language());
String[] voice = loc.split("-");
Locale l = null;
if(voice.length == 1)
l = new Locale(voice[0]);
else if(voice.length == 2)
l = new Locale(voice[0], voice[1]);
else if(voice.length == 3)
l = new Locale(voice[0], voice[1], voice[2]);
return l;
}
public static boolean isVersionLessThan(String v1, String v2) {
VersionComparator cmp = new VersionComparator();
return cmp.compare(v1, v2) < 0;
}
public static void showNoWifiDialog(Context context) {
AlertDialog.Builder usageDialog = new AlertDialog.Builder(context);
usageDialog.setTitle(R.string.no_wifi_connection);
usageDialog.setMessage(R.string.no_wifi_connection_message);
usageDialog.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
usageDialog.show();
}
public static boolean isApplicationVisible() {
return isApplicationVisible;
}
public static void applicationResumed() {
isApplicationVisible = true;
}
public static void applicationPaused() {
isApplicationVisible = false;
}
public static String secondsToTimecode(double _seconds) {
ArrayList<String> timecode = new ArrayList<String>();
double hours, minutes, seconds = 0;
hours = Math.floor(_seconds/3600);
minutes = Math.floor((_seconds - (hours * 3600)) / 60);
seconds = Math.floor(_seconds - (hours * 3600) - (minutes * 60));
if(hours > 0)
timecode.add(String.valueOf((int)hours));
timecode.add(twoDigitsInt((int)minutes));
timecode.add(twoDigitsInt((int) seconds));
return TextUtils.join(":", timecode);
}
private static String twoDigitsInt( int pValue )
{
if ( pValue == 0 )
return "00";
if ( pValue < 10 )
return "0" + pValue;
return String.valueOf( pValue );
}
public static String generateRandomString() {
SecureRandom random = new SecureRandom();
return new BigInteger(130, random).toString(32).substring(0, 12);
}
public Bitmap getCachedBitmap(String key) {
if(key == null)
return null;
Bitmap bitmap = null;
try {
// Logger.d("Trying to get cached thumb: %s", key);
SimpleDiskCache.BitmapEntry bitmapEntry = mSimpleDiskCache.getBitmap(key);
// Logger.d("bitmapEntry: %s", bitmapEntry);
if(bitmapEntry != null) {
bitmap = bitmapEntry.getBitmap();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return bitmap;
}
public void fetchMediaThumb(final PlexMedia media, final int width, final int height, final String whichThumb, final String key, final BitmapHandler bitmapHandler) {
if(whichThumb == null)
return;
Logger.d("Fetching media thumb for %s at %dx%d with key %s", media.getTitle(), width, height, key);
Bitmap bitmap = getCachedBitmap(key);
if(bitmap == null) {
media.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(final Connection connection) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Logger.d("No cached bitmap found, fetching");
InputStream is = media.getThumb(width, height, whichThumb, connection);
try {
Logger.d("Saving cached bitmap with key %s", key);
mSimpleDiskCache.put(key, is);
fetchMediaThumb(media, width, height, whichThumb, key, bitmapHandler);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
@Override
public void onFailure(int statusCode) {
Logger.d("Failed to find server connection for %s while searching for thumb for %s", media.server.name, media.getTitle());
}
});
} else {
Logger.d("Found cached bitmap");
if(bitmapHandler != null)
bitmapHandler.onSuccess(bitmap);
}
}
public void fetchNotificationBitmap(final PlexMedia.IMAGE_KEY key, final PlexMedia media, final Runnable onFinish) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
media.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
InputStream inputStream = media.getNotificationThumb(key, connection);
if(inputStream != null) {
try {
inputStream.reset();
} catch (IOException e) {
}
try {
Logger.d("image key: %s", media.getImageKey(key));
mSimpleDiskCache.put(media.getImageKey(key), inputStream);
inputStream.close();
if(onFinish != null)
onFinish.run();
} catch (Exception e) {
}
}
}
@Override
public void onFailure(int statusCode) {
}
});
return null;
}
}.execute();
}
public void setNotification(final PlexClient client, final PlayerState currentState,
final PlexMedia media, final ArrayList<? extends PlexMedia> playlist, final MediaSessionCompat mediaSession) {
if(client == null) {
logger.d("Client is null for some reason");
return;
}
if(client.isLocalDevice())
return;
final int[] numBitmapsFetched = new int[]{0};
final int[] numBitmapsToFetch = new int[]{0};
// Figure out which track in the playlist this is, to know whether or not to set up the Intent to handle previous/next
final int[] playlistIndexF = new int[1];
for(int i=0;i<playlist.size();i++) {
// logger.d("comparing %s to %s (%s)", playlist.get(i).key, media.key, playlist.get(i).key.equals(media.key));
if(playlist.get(i).equals(media)) {
// logger.d("found a match for %s", media.key);
playlistIndexF[0] = i;
break;
}
}
final int playlistIndex = playlistIndexF[0];
PlexMedia.IMAGE_KEY keyIcon = media instanceof PlexTrack ? PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB_MUSIC_BIG : PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB_BIG;
PlexMedia.IMAGE_KEY keyBackground = media instanceof PlexTrack ? PlexMedia.IMAGE_KEY.NOTIFICATION_MUSIC_BACKGROUND : PlexMedia.IMAGE_KEY.NOTIFICATION_BACKGROUND;
final Bitmap[] bitmapsFetched = new Bitmap[2];
final Runnable onFinished = () -> {
// this is where the magic happens!
android.content.Intent playIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
playIntent.setAction(Intent.ACTION_PLAY);
playIntent.putExtra(SubscriptionService.CLIENT, client);
playIntent.putExtra(SubscriptionService.MEDIA, media);
playIntent.putParcelableArrayListExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent playPendingIntent = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, playIntent, PendingIntent.FLAG_UPDATE_CURRENT);
android.content.Intent pauseIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
pauseIntent.setAction(Intent.ACTION_PAUSE);
pauseIntent.putExtra(SubscriptionService.CLIENT, client);
pauseIntent.putExtra(SubscriptionService.MEDIA, media);
pauseIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent pausePendingIntent = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, pauseIntent, PendingIntent.FLAG_UPDATE_CURRENT);
android.content.Intent disconnectIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
disconnectIntent.setAction(Intent.ACTION_DISCONNECT);
disconnectIntent.putExtra(SubscriptionService.CLIENT, client);
disconnectIntent.putExtra(SubscriptionService.MEDIA, media);
disconnectIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent piDisconnect = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, disconnectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
android.content.Intent nowPlayingIntent = new android.content.Intent(VoiceControlForPlexApplication.this, MainActivity.class);
nowPlayingIntent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK |
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK);
nowPlayingIntent.setAction(MainActivity.ACTION_SHOW_NOW_PLAYING);
nowPlayingIntent.putExtra(Intent.EXTRA_MEDIA, media);
nowPlayingIntent.putExtra(Intent.EXTRA_CLIENT, client);
nowPlayingIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent piNowPlaying = PendingIntent.getActivity(VoiceControlForPlexApplication.this, 0, nowPlayingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
android.content.Intent rewindIntent;
android.content.Intent forwardIntent;
android.content.Intent previousIntent;
android.content.Intent nextIntent;
List<NotificationCompat.Action> actions = new ArrayList<>();
if(!media.isMusic()) {
rewindIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
rewindIntent.setAction(Intent.ACTION_REWIND);
rewindIntent.putExtra(SubscriptionService.CLIENT, client);
rewindIntent.putExtra(SubscriptionService.MEDIA, media);
rewindIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent piRewind = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, rewindIntent, PendingIntent.FLAG_UPDATE_CURRENT);
forwardIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
forwardIntent.setAction(Intent.ACTION_FORWARD);
forwardIntent.putExtra(SubscriptionService.CLIENT, client);
forwardIntent.putExtra(SubscriptionService.MEDIA, media);
forwardIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
PendingIntent piForward = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, forwardIntent, PendingIntent.FLAG_UPDATE_CURRENT);
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_rewind, "Rewind", piRewind).build());
if(currentState == PlayerState.PAUSED)
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_play, "Play", playPendingIntent).build());
else
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_pause, "Pause", pausePendingIntent).build());
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_forward, "Fast Forward", piForward).build());
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_disconnect, "Disconnect", piDisconnect).build());
} else {
PendingIntent piPrevious = null;
if(playlistIndex > 0) {
previousIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
previousIntent.setAction(Intent.ACTION_PREVIOUS);
previousIntent.putExtra(SubscriptionService.CLIENT, client);
previousIntent.putExtra(SubscriptionService.MEDIA, media);
previousIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
piPrevious = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, previousIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
PendingIntent piNext = null;
if(playlistIndex+1 < playlist.size()) {
nextIntent = new android.content.Intent(VoiceControlForPlexApplication.this, client.isLocalClient ? LocalMusicService.class : SubscriptionService.class);
nextIntent.setAction(Intent.ACTION_NEXT);
nextIntent.putExtra(SubscriptionService.CLIENT, client);
nextIntent.putExtra(SubscriptionService.MEDIA, media);
nextIntent.putExtra(Intent.EXTRA_PLAYLIST, playlist);
piNext = PendingIntent.getService(VoiceControlForPlexApplication.this, 0, nextIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_previous, "Previous", piPrevious).build());
if(currentState == PlayerState.PAUSED)
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_play, "Play", playPendingIntent).build());
else
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_pause, "Pause", pausePendingIntent).build());
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_next, "Next", piNext).build());
actions.add(new NotificationCompat.Action.Builder(R.drawable.notif_disconnect, "Disconnect", piDisconnect).build());
}
Notification n;
NotificationCompat.Builder builder = new NotificationCompat.Builder(VoiceControlForPlexApplication.this);
// Show controls on lock screen even when user hides sensitive content.
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.vcfp_notification)
.setAutoCancel(false)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setDefaults(Notification.DEFAULT_ALL)
// Add media control buttons that invoke intents in your media service
// Apply the media style template
.setStyle(new NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1, 2 /* #1: pause button */)
.setMediaSession(mediaSession.getSessionToken()))
.setContentTitle(media.getNotificationTitle())
.setContentText(media.getNotificationSubtitle())
.setContentIntent(piNowPlaying)
.setLargeIcon(bitmapsFetched[0]);
for (NotificationCompat.Action action : actions) {
builder.addAction(action);
}
n = builder.build();
try {
// Disable notification sound
n.defaults = 0;
mNotifyMgr.notify(nowPlayingNotificationId, n);
Logger.d("Notification set");
} catch (Exception ex) {
ex.printStackTrace();
}
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmapsFetched[1])
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, media.art)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, media.getNotificationTitle())
.build()
);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(
currentState == PlayerState.PAUSED ? PlaybackStateCompat.STATE_PAUSED : PlaybackStateCompat.STATE_PLAYING, Long.parseLong(media.viewOffset), 1f).build());
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
};
// Check if the bitmaps have already been fetched. If they have, launch the runnable to set the notification
Bitmap bitmap = getCachedBitmap(media.getImageKey(keyIcon));
Bitmap bigBitmap = getCachedBitmap(media.getImageKey(keyBackground));
if(bitmap != null && bigBitmap != null) {
bitmapsFetched[0] = bitmap;
bitmapsFetched[1] = bigBitmap;
onFinished.run();
} else {
ArrayList<FetchMediaImageTask> taskList = new ArrayList<>();
if(media.getNotificationThumb(keyIcon) != null) {
numBitmapsToFetch[0]++;
// Fetch both (big and regular) versions of the notification bitmap, and when both are finished, launch the runnable above that will set the notification
taskList.add(new FetchMediaImageTask(media, PlexMedia.IMAGE_SIZES.get(keyIcon)[0], PlexMedia.IMAGE_SIZES.get(keyIcon)[1], media.getNotificationThumb(keyIcon), media.getImageKey(keyIcon), new BitmapHandler() {
@Override
public void onSuccess(Bitmap bitmap) {
bitmapsFetched[0] = bitmap;
if (numBitmapsFetched[0] + 1 == numBitmapsToFetch[0])
onFinished.run();
numBitmapsFetched[0]++;
}
}));
}
if(media.getNotificationThumb(keyBackground) != null) {
numBitmapsToFetch[0]++;
taskList.add(new FetchMediaImageTask(media, PlexMedia.IMAGE_SIZES.get(keyBackground)[0], PlexMedia.IMAGE_SIZES.get(keyBackground)[1], media.getNotificationThumb(keyBackground), media.getImageKey(keyBackground), new BitmapHandler() {
@Override
public void onSuccess(Bitmap bitmap) {
bitmapsFetched[1] = bitmap;
if (numBitmapsFetched[0] + 1 == numBitmapsToFetch[0])
onFinished.run();
numBitmapsFetched[0]++;
}
}));
}
if(taskList.size() == 0)
onFinished.run();
else
for(FetchMediaImageTask task : taskList)
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
public void cancelNotification() {
mNotifyMgr.cancel(nowPlayingNotificationId);
if(hasWear()) {
new SendToDataLayerThread(WearConstants.MEDIA_STOPPED, this).start();
}
}
public void setupInAppPurchasing() {
mIabHelper = new IabHelper(this, base64EncodedPublicKey);
mIabHelper.enableDebugLogging(false);
mIabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
Logger.d("IABHelper Setup finished.");
// Logger.d("Hash: %s", getEmailHash());
if (!result.isSuccess()) {
// Oh noes, there was a problem.
Logger.d("Problem setting up in-app billing: %s", result);
return;
}
// Have we been disposed of in the meantime? If so, quit.
if (mIabHelper == null) return;
promoReceiver = new IabBroadcastReceiver(new IabBroadcastReceiver.IabBroadcastListener() {
@Override
public void receivedBroadcast() {
mIabHelper.queryInventoryAsync(mGotInventoryListener);
}
});
IntentFilter broadcastFilter = new IntentFilter(IabBroadcastReceiver.ACTION);
registerReceiver(promoReceiver, broadcastFilter);
// IAB is fully set up. Now, let's get an inventory of stuff we own.
Logger.d("Setup successful. Querying inventory.");
mIabHelper.queryInventoryAsync(mGotInventoryListener);
}
});
}
public void refreshInAppInventory() {
if(mIabHelper != null && iabHelperSetupDone)
mIabHelper.queryInventoryAsync(mGotInventoryListener);
}
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, final Inventory inventory) {
// Have we been disposed of in the meantime? If so, quit.
if (mIabHelper == null) return;
// Is it a failure?
if (result.isFailure()) {
Logger.d("Failed to query inventory: " + result);
return;
}
iabHelperSetupDone = true;
Logger.d("Query inventory was successful.");
if(mIabHelper != null)
mIabHelper.flagEndAsync();
// Get the price for chromecast & wear support
mIabHelper.queryInventoryAsync(true, new ArrayList<>(Arrays.asList(SKU_CHROMECAST, SKU_WEAR, SKU_LOCALMEDIA)), new IabHelper.QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
SkuDetails skuDetails = inv.getSkuDetails(SKU_CHROMECAST);
if(skuDetails != null) {
mChromecastPrice = skuDetails.getPrice();
}
skuDetails = inv.getSkuDetails(SKU_WEAR);
if(skuDetails != null) {
mWearPrice = skuDetails.getPrice();
}
skuDetails = inv.getSkuDetails(SKU_LOCALMEDIA);
if(skuDetails != null)
mLocalMediaPrice = skuDetails.getPrice();
// If the SKU being used is the test sku, consume it so that it has to be bought each time the app is run
if(SKU_CHROMECAST == SKU_TEST_PURCHASED || SKU_WEAR == SKU_TEST_PURCHASED || SKU_LOCALMEDIA == SKU_TEST_PURCHASED) {
if (inventory.hasPurchase(SKU_TEST_PURCHASED))
mIabHelper.consumeAsync(inventory.getPurchase(SKU_TEST_PURCHASED),null);
} else {
if(BuildConfig.CHROMECAST_REQUIRES_PURCHASE) {
Purchase chromecastPurchase = inventory.getPurchase(SKU_CHROMECAST);
mHasChromecast = (chromecastPurchase != null && verifyDeveloperPayload(chromecastPurchase));
}
if(BuildConfig.WEAR_REQUIRES_PURCHASE) {
Purchase wearPurchase = inventory.getPurchase(SKU_WEAR);
mHasWear = (wearPurchase != null && verifyDeveloperPayload(wearPurchase));
}
if(BuildConfig.LOCALMEDIA_REQUIRES_PURCHASE) {
Purchase localmediaPurchase = inventory.getPurchase(SKU_LOCALMEDIA);
mHasLocalMedia = (localmediaPurchase != null && verifyDeveloperPayload(localmediaPurchase));
}
}
Logger.d("Has Chromecast: %s", mHasChromecast);
Logger.d("Has Wear: %s", mHasWear);
logger.d("Has Localmedia: %s", mHasLocalMedia);
Logger.d("Initial inventory query finished.");
inventoryQueried = true;
onInventoryQueryFinished();
}
});
}
};
public boolean getInventoryQueried() {
return inventoryQueried;
}
// This runs once we have queried the play store to see if chromecast or wear support have been purchased.
// If Wear support has not been purchased, we can attempt to contact a connected Wear device, and if we hear back,
// we can throw a popup alerting the user that the app supports Wear
private void onInventoryQueryFinished() {
if(!mHasWear) {
Logger.d("[VoiceControlForPlexApplication] Sending ping");
new SendToDataLayerThread(WearConstants.PING, this).start();
} else {
if(mainActivity != null) {
mainActivity.hidePurchaseWearMenuItem();
}
}
}
public void setOnHasWearActivity(MainActivity activity) {
mainActivity = activity;
}
boolean verifyDeveloperPayload(Purchase p) {
return p.getDeveloperPayload().equals(SKU_TEST_PURCHASED == SKU_CHROMECAST ? getEmailHash() : "");
}
public String getEmailHash() {
AccountManager manager = (AccountManager) getSystemService(ACCOUNT_SERVICE);
Account[] list = manager.getAccounts();
String userEmail = "";
for(Account account : list) {
if(account.type.equals("com.google")) {
userEmail = account.name;
break;
}
}
String hash = "";
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.reset();
byte[] buffer = userEmail.getBytes();
md.update(buffer);
byte[] digest = md.digest();
for (int i = 0; i < digest.length; i++) {
hash += Integer.toString( ( digest[i] & 0xff ) + 0x100, 16).substring( 1 );
}
} catch (Exception ex) {
ex.printStackTrace();
}
Logger.d("hash: %s", hash);
return hash;
}
public IabHelper getIabHelper() {
return mIabHelper;
}
public void setHasChromecast(boolean hasChromecast) {
mHasChromecast = hasChromecast;
}
public void setHasWear(boolean hasWear) {
mHasWear = hasWear;
}
public void setHasLocalmedia(boolean hasLocalmedia) {
mHasLocalMedia = hasLocalmedia;
}
public static String getChromecastPrice() {
return mChromecastPrice;
}
public static String getWearPrice() {
return mWearPrice;
}
public static String getLocalmediaPrice() {
return mLocalMediaPrice;
}
public static Map<String, PlexClient> getAllClients() {
Map<String, PlexClient> allClients = new HashMap<String, PlexClient>();
PlexClient localDevice = PlexClient.getLocalPlaybackClient();
allClients.put(localDevice.name, localDevice);
allClients.putAll(clients);
allClients.putAll(castClients);
return allClients;
}
public interface NetworkChangeListener {
void onConnected(int connectionType);
void onDisconnected();
}
public void onNetworkConnected(int connectionType) {
if(networkChangeListener != null) {
networkChangeListener.onConnected(connectionType);
}
}
public void onNetworkDisconnected() {
if(networkChangeListener != null)
networkChangeListener.onDisconnected();
}
public void setNetworkChangeListener(NetworkChangeListener listener) {
networkChangeListener = listener;
}
public void getWearMediaImage(final PlexMedia media, final BitmapHandler onFinished) {
Bitmap bitmap = getCachedBitmap(media.getImageKey(PlexMedia.IMAGE_KEY.WEAR_BACKGROUND));
if(bitmap == null) {
fetchNotificationBitmap(PlexMedia.IMAGE_KEY.WEAR_BACKGROUND, media, new Runnable() {
@Override
public void run() {
Bitmap bitmap = getCachedBitmap(media.getImageKey(PlexMedia.IMAGE_KEY.WEAR_BACKGROUND));
Logger.d("Done fetching bitmap from cache, got: %s", bitmap);
onFinished.onSuccess(bitmap);
}
});
} else {
Logger.d("Bitmap was already in cache: %s", bitmap);
onFinished.onSuccess(bitmap);
}
}
public static Asset createAssetFromBitmap(Bitmap bitmap) {
final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);
return Asset.createFromBytes(byteStream.toByteArray());
}
public static void setWearMediaTitles(DataMap dataMap, PlexMedia media) {
if(media.isShow()) {
dataMap.putString(WearConstants.MEDIA_TITLE, media.getTitle());
dataMap.putString(WearConstants.MEDIA_SUBTITLE, media.getEpisodeTitle());
} else if(media.isMovie() || media.isClip()) {
dataMap.putString(WearConstants.MEDIA_TITLE, media.title);
dataMap.remove(WearConstants.MEDIA_SUBTITLE);
} else if(media.isMusic()) {
dataMap.putString(WearConstants.MEDIA_TITLE, media.grandparentTitle);
dataMap.putString(WearConstants.MEDIA_SUBTITLE, media.title);
}
}
public static String getUUID() {
String uuid = VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.UUID, null);
if(uuid == null) {
uuid = UUID.randomUUID().toString();
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.UUID, uuid);
}
return uuid;
}
public static QueryString getPlaybackQueryString(PlexMedia media,
MediaContainer mediaContainer, Connection connection,
String transientToken, PlexDirectory album,
boolean resumePlayback) {
QueryString qs = new QueryString("machineIdentifier", media.server.machineIdentifier);
qs.add("key", media.key);
qs.add("containerKey", String.format("/playQueues/%s", mediaContainer.playQueueID));
qs.add("port", connection.port);
qs.add("address", connection.address);
if (transientToken != null)
qs.add("token", transientToken);
if (media.server.accessToken != null)
qs.add(PlexHeaders.XPlexToken, media.server.accessToken);
if (album != null)
qs.add("containerKey", album.key);
// new for PMP:
qs.add("commandID", "0");
qs.add("type", media.getType().equals("music") ? "music" : "video");
qs.add("protocol", "http");
qs.add("offset", resumePlayback && media.viewOffset != null ? media.viewOffset : "0");
return qs;
}
public boolean isLoggedIn() {
return getInstance().prefs.getString(Preferences.AUTHENTICATION_TOKEN) != null;
}
public static PlexServer getServerByMachineIdentifier(String machineIdentifier) {
if(machineIdentifier == null)
return null;
for(PlexServer server : servers.values()) {
if(server.machineIdentifier != null && server.machineIdentifier.equals(machineIdentifier)) {
return server;
}
}
return null;
}
public void checkImageCacheVersion() {
if(prefs.get(Preferences.IMAGE_CACHE_VERSION, 0) < currentImageCacheVersion) {
try {
Logger.d("Clearing cache");
mSimpleDiskCache.clear();
prefs.put(Preferences.IMAGE_CACHE_VERSION, currentImageCacheVersion);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void handleUncaughtException (Thread thread, Throwable e) {
e.printStackTrace();
prefs.put(Preferences.CRASHED, true);
}
@Override
public void onTerminate() {
super.onTerminate();
if(promoReceiver != null)
unregisterReceiver(promoReceiver);
if(mIabHelper != null)
mIabHelper.dispose();
mIabHelper = null;
}
public int getSecondsSinceLastServerScan() {
Date now = new Date();
Date lastServerScan = new Date(prefs.get(Preferences.LAST_SERVER_SCAN, 0l));
Logger.d("now: %s", now);
Logger.d("lastServerScan: %s", lastServerScan);
return (int)((now.getTime() - lastServerScan.getTime())/1000);
}
public String getUserThumbKey() {
String key = "user_thumb_key";
if(prefs.getString(Preferences.PLEX_USERNAME) != null)
key += prefs.getString(Preferences.PLEX_USERNAME);
return key;
}
public static int[] getScreenDimensions(Context context) {
int screenWidth = 0, screenHeight = 0;
final DisplayMetrics metrics = new DisplayMetrics();
WindowManager window = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
Display display = window.getDefaultDisplay();
Method mGetRawH = null, mGetRawW = null;
try {
// For JellyBean 4.2 (API 17) and onward
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
display.getRealMetrics(metrics);
screenWidth = metrics.widthPixels;
screenHeight = metrics.heightPixels;
} else {
mGetRawH = Display.class.getMethod("getRawHeight");
mGetRawW = Display.class.getMethod("getRawWidth");
try {
screenWidth = (Integer) mGetRawW.invoke(display);
screenHeight = (Integer) mGetRawH.invoke(display);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (NoSuchMethodException e3) {
e3.printStackTrace();
}
return new int[]{ screenWidth, screenHeight };
}
public HashMap<String, Calendar> getActiveConnectionExpiresList() {
Type calType = new TypeToken<HashMap<String, Calendar>>() {}.getType();
String json = VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.ACTIVE_CONNECTION_EXPIRES, null);
return json == null ? new HashMap<String, Calendar>() : (HashMap<String, Calendar>)VoiceControlForPlexApplication.gsonRead.fromJson(json, calType);
}
public HashMap<String, Connection> getActiveConnectionList() {
Type conType = new TypeToken<HashMap<String, Connection>>() {}.getType();
String json = VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.ACTIVE_CONNECTION, null);
return json == null ? new HashMap<String, Connection>() : (HashMap<String, Connection>)VoiceControlForPlexApplication.gsonRead.fromJson(json, conType);
}
public void saveActiveConnection(PlexServer server, Connection connection, Calendar expires) {
HashMap<String, Connection> connectionList = getActiveConnectionList();
connectionList.put(server.machineIdentifier, connection);
Type conType = new TypeToken<HashMap<String, Connection>>(){}.getType();
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.ACTIVE_CONNECTION,
VoiceControlForPlexApplication.gsonWrite.toJson(connectionList, conType));
HashMap<String, Calendar> expiresList = getActiveConnectionExpiresList();
expiresList.put(server.machineIdentifier, expires);
Type calType = new TypeToken<HashMap<String, Calendar>>(){}.getType();
VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.ACTIVE_CONNECTION_EXPIRES,
VoiceControlForPlexApplication.gsonWrite.toJson(expiresList, calType));
}
public static int getOrientation() {
try {
return getInstance().getResources().getConfiguration().orientation;
} catch (Exception e) {}
return Configuration.ORIENTATION_PORTRAIT;
}
public static String[] getMediaPosterPrefs(PlexMedia media) {
String widthPref, heightPref;
if(media.isMusic()) {
widthPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.MUSIC_POSTER_WIDTH : Preferences.MUSIC_POSTER_WIDTH_LAND;
heightPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.MUSIC_POSTER_HEIGHT : Preferences.MUSIC_POSTER_HEIGHT_LAND;
} else if(media.isShow()) {
widthPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.SHOW_POSTER_WIDTH : Preferences.SHOW_POSTER_WIDTH_LAND;
heightPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.SHOW_POSTER_HEIGHT : Preferences.SHOW_POSTER_HEIGHT_LAND;
} else {
widthPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.MOVIE_POSTER_WIDTH : Preferences.MOVIE_POSTER_WIDTH_LAND;
heightPref = getOrientation() != Configuration.ORIENTATION_LANDSCAPE ? Preferences.MOVIE_POSTER_HEIGHT : Preferences.MOVIE_POSTER_HEIGHT_LAND;
}
return new String[]{widthPref, heightPref};
}
public void sendWearPlaybackChange(final PlayerState state, PlexMedia media) {
if(hasWear()) {
logger.d("Sending Wear Notification: %s", state);
final DataMap data = new DataMap();
String msg = null;
if (state == PlayerState.PLAYING) {
data.putString(WearConstants.MEDIA_TYPE, media.getType());
VoiceControlForPlexApplication.setWearMediaTitles(data, media);
msg = WearConstants.MEDIA_PLAYING;
} else if (state == PlayerState.STOPPED) {
msg = WearConstants.MEDIA_STOPPED;
} else if (state == PlayerState.PAUSED) {
msg = WearConstants.MEDIA_PAUSED;
VoiceControlForPlexApplication.setWearMediaTitles(data, media);
}
if (msg != null) {
if (msg.equals(WearConstants.MEDIA_PLAYING)) {
getWearMediaImage(media, new BitmapHandler() {
@Override
public void onSuccess(Bitmap bitmap) {
DataMap binaryDataMap = new DataMap();
binaryDataMap.putAll(data);
binaryDataMap.putAsset(WearConstants.IMAGE, VoiceControlForPlexApplication.createAssetFromBitmap(bitmap));
binaryDataMap.putString(WearConstants.PLAYBACK_STATE, state.name());
new SendToDataLayerThread(WearConstants.RECEIVE_MEDIA_IMAGE, binaryDataMap, getInstance()).sendDataItem();
}
});
}
new SendToDataLayerThread(msg, data, this).start();
}
}
}
}